#include "movie.h"

/* A full-screen SMK player */

// Dependency on libsmacker
#include <Smacker/smacker.h>

// Send in the externs
extern struct cfg_struct *cfg;
extern GLfloat aspect;

/* A structure to hold a block of decoded audio data.
	These go into a queue as they are decoded. */
struct smk_audio_stream_component
{
	unsigned char *data;
	int len;
	struct smk_audio_stream_component *next;
};

/* Master structure for decoded audio queue.
	As blocks are decoded, they go to the queue at *tail.
	They are consumed from *head. */
struct smk_audio_stream
{
	Uint16 in_format;
	int in_channels;
	int in_rate;

	Uint16 out_format;
	int out_channels;
	int out_rate;

	// interpolation: store last-written value
	short last_val[2];

	// asc queue
	struct smk_audio_stream_component *head, *tail;
	// pointer to current offset within asc data-block
	int index;
};

/* Advance the audio stream by removing the node
	at *head and advancing to more audio. */
static short smk_audio_stream_read (struct smk_audio_stream *s)
{
	unsigned char retval;

	// check if enough bytes are left to read
		// also bail on empty stream
		// this loop will skip past NULL blocks in stream as well
	while (s->head != NULL && s->index >= s->head->len)
	{
		// track next pointer
		struct smk_audio_stream_component *cur = s->head->next;

		// free existing data (if not null... don't crash on problem decodes)
		// then free head, then update head pointer
		if (s->head->data != NULL) free(s->head->data);
		free(s->head);
		s->head = cur;

		// reset pointer to beginning of block
		s->index = 0;
	}

	// bail on out-of-data
	if (s->head == NULL) return -1;

	retval = s->head->data[s->index];
	s->index ++;

	return retval;
}

static short as_read_u8 (struct smk_audio_stream *s)
{
	short byte = smk_audio_stream_read(s);

	if (byte < 0)
	{
		return 0;
	} else {
		// converts a u8 into a s16
		return ((byte << 8) | byte) - 32768;
	}
}

static short as_read_s16(struct smk_audio_stream *s)
{
	short lower = smk_audio_stream_read(s);
	short upper = smk_audio_stream_read(s);

	if (lower < 0 || upper < 0)
	{
		return 0;
	} else {
		// reads an s16 directly into a s16
		return lower | (upper << 8);
	}
}

static void movie_music_hook(void *ud, Uint8 *stream, int len)
{
	int i = 0,j,k;

	/* rate conversion vars */
	char rate_conv_mode;
	unsigned char samples_to_read, samples_to_write;

	// holds read values
	int in_val[8][2];
	// holds output values
	int out_val[8][2];

	// used by 16-bit conversion
	unsigned short val16;

	struct smk_audio_stream *udata = ud;

	/* Audio processing is handled by upconverting input samples
		to S16LSB and 2-channel.  It retains its own rate.
		We need to figure out how to convert that to
		our desired output rate. */
	if (udata->in_rate > udata->out_rate)
	{
		/* Input rate is greater than output rate.
			Downsample (Average N output samples) to compensate. */
		rate_conv_mode = -1;
		samples_to_read = udata->in_rate / udata->out_rate;
		samples_to_write = 1;
	} else if (udata->in_rate < udata->out_rate) {
		/* Input rate is less than output rate.
			Upsample (Interpolate N output samples) to compensate. */
		rate_conv_mode = 1;
		samples_to_read = 1;
		samples_to_write = udata->out_rate / udata->in_rate;
	} else {
		/* Input rate exactly matches output rate.
			No rate-conversion necessary. */
		rate_conv_mode = 0;
		samples_to_read = 1;
		samples_to_write = 1;
	}

	if (samples_to_read > 8 || samples_to_write > 8)
	{
		fprintf(stderr,"Error: rate conversion would require reading %d / writing %d: ratio too extreme\n",samples_to_read,samples_to_write);
		memset(stream,0,len);
		return;
	}

	// loop to fill up the audio buffer
	while (i < len)
	{
		for (j = 0; j < samples_to_read; j ++)
		{
			/* Read N sample from movie file */
			switch(udata->in_format)
			{
				case AUDIO_U8:
					in_val[j][0] = as_read_u8(udata);
					break;
				case AUDIO_S16LSB:
					in_val[j][0] = as_read_s16(udata);
					break;
				default:
					/* should never happen, but don't dump static */
					in_val[j][0] = 0;
			}

			/* if input is stereo, read again... else just copy l-channel to r-channel */
			if (udata->in_channels == 2)
			{
				switch(udata->in_format)
				{
					case AUDIO_U8:
						in_val[j][1] = as_read_u8(udata);
						break;
					case AUDIO_S16LSB:
						in_val[j][1] = as_read_s16(udata);
						break;
					default:
						in_val[j][1] = 0;
				}
			} else {
				in_val[j][1] = in_val[j][0];
			}
		}

		// process data
		if (rate_conv_mode > 0)
		{
			// upsample: interpolate
			for (j = 0; j < samples_to_write; j ++)
			{
				out_val[j][0] = ((udata->last_val[0] * (samples_to_write - j)) / samples_to_write) +
								((in_val[0][0] * j) / samples_to_write);
				out_val[j][1] = ((udata->last_val[1] * (samples_to_write - j)) / samples_to_write) +
								((in_val[0][1] * j) / samples_to_write);
			}
		} else if (rate_conv_mode < 0) {
			// downsample: average
			int sum[2] = {0,0};
			for (j = 0; j < samples_to_read; j ++)
			{
				sum[0] += in_val[j][0];
			}
			out_val[0][0] = sum[0] / samples_to_read;
			out_val[0][1] = sum[1] / samples_to_read;
		}

		for (j = 0; j < samples_to_write; j ++)
		{
			// output processing: fold stereo to mono
			if (udata->out_channels == 1)
			{
				out_val[j][0] = (out_val[j][0] + out_val[j][1]) / 2;
			}

			// output processing: bit depth conversion and write
			for (k = 0; k < udata->out_channels; k ++)
			{
				/* Write a sample, do any conversion needed */
				switch(udata->out_format)
				{
					case AUDIO_U8:
						//             reduce to 8 bit    unsign
						stream[i] = (out_val[j][k] % 256) + 128;
						i ++;
						break;
					case AUDIO_S8:
						//             reduce to 8 bit
						stream[i] = (out_val[j][k] % 256);
						i ++;
						break;
					case AUDIO_U16LSB:
						//          unsign
						val16 = out_val[j][k] + 32768;
						stream[i] = val16 & 0xFF;
						i ++;
						stream[i] = val16 >> 8;
						i ++;
						break;
					case AUDIO_S16LSB:
						stream[i] = out_val[j][k] & 0xFF;
						i ++;
						stream[i] = out_val[j][k] >> 8;
						i ++;
						break;
					case AUDIO_U16MSB:
						//         unsign
						val16 = out_val[j][k] + 32768;
						stream[i] = val16 >> 8;
						i ++;
						stream[i] = val16 & 0xFF;
						i ++;
						break;
					case AUDIO_S16MSB:
						stream[i] = out_val[j][k] >> 8;
						i ++;
						stream[i] = out_val[j][k] & 0xFF;
						i ++;
						break;
				}
			}
		}

		// store last-val for next loop's lerp
		udata->last_val[0] = out_val[samples_to_write - 1][0];
		udata->last_val[1] = out_val[samples_to_write - 1][1];

	}
}

int movie_main(char *movie_name, int next_gamestate)
{
	// set to 0 to run.  set to non-zero to exit and change gamestate
	int done = 0;

	// file name of movie to play
	char movie_path[128] = "";

	// Video
	smk s;
	unsigned long w, h;
	double usf;
	unsigned char a_t, a_d[7],a_c[7];
	unsigned long a_r[7];
	// timer - auto callback per frame
	SDL_TimerID frame_timer;

	// Integer which holds the current frame texture
	SDL_Surface *frame = NULL;
	GLuint frame_tex;
	// Used for centering the video on screen
	GLint xoffset, yoffset;

	// Audio: this audio stream is used to play music
	unsigned char audio_exists = 0;
	struct smk_audio_stream aud_buf;

	/* Go open the SMK file. */
	sprintf(movie_path,"data/video/%s.smk",movie_name);
	if (!(s = smk_open_file(movie_path,SMK_MODE_DISK)))
	{
		/* Unable to load SMK file. Just move on, this is not important. */
		printf("WARNING: Unable to load %s, moving on.\n",movie_path);
		return next_gamestate;
	}

	// get movie info
	smk_info_all(s,NULL,NULL,&usf);
	smk_info_video(s,&w,&h,NULL);
	smk_info_audio(s,&a_t,a_c,a_d,a_r);

	/* Put us into GLOrtho mode */
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	/* Using OpenGL's native scaling for our movie */
	float movie_aspect = (float)w / h;
	if (movie_aspect > aspect) /* 16:9 movie on narrow screen: match W, scale H */
	{
	  glOrtho(0.0f, (float)w, w / aspect, 0.0f, -1.0f, 1.0f);
	  xoffset = 0;
	  yoffset = (((w / aspect) - h) / 2.0) + .5;
	}
	else /* 4:3 movie on wider screen: match H, scale W */
	{
	  glOrtho(0.0f, aspect * h, (float)h, 0.0f, -1.0f, 1.0f);
	  xoffset = (((h * aspect) - w) / 2.0) + .5;
	  yoffset = 0;
	}
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	// all is well, enable video rendering
	smk_enable_video(s,1);

	// enable audio rendering for track 0 if available
	if (a_t & SMK_AUDIO_TRACK_0) audio_exists = 1;
	if (audio_exists) smk_enable_audio(s,0,1);

	/* Retrieve first frame from movie. */
	smk_first(s);

	/* Convert SMK frame to an SDL surface */
	frame = tex_from_smkframe(smk_get_palette(s),smk_get_video(s),w,h);
	if (frame == NULL)
	{
		fprintf(stderr,"WARNING: tex_from_smkframe returned NULL.\n");
		done = next_gamestate;
	} else {
		/* Upload SDL surface to gfx card (gl texture) */
		frame_tex = build_texture (frame, GL_LINEAR);
		/* Go ahead and bind to texture now, movie scenes have only one tex */
		glBindTexture(GL_TEXTURE_2D,frame_tex);
		/* Once it's on the GFX card throw away the SDL surface */
		SDL_FreeSurface(frame);

		/* Display list for the entire movie scene. */
		GLuint quad_list = glGenLists(1);
		glNewList(quad_list, GL_COMPILE);
			glBox(xoffset,yoffset,w,h,0,0,powerOfTwo(w),powerOfTwo(h));
		glEndList();

		glClear(GL_COLOR_BUFFER_BIT);

		/* Put first frame on the screen */
		glCallList(quad_list);

		SDL_GL_SwapBuffers();

		if (audio_exists)
		{
			/* set up the audio buffer "conversion params" */
			aud_buf.in_channels = a_c[0];
			aud_buf.in_format = ((a_d[0] == 8) ? AUDIO_U8 : AUDIO_S16LSB);
			aud_buf.in_rate = a_r[0];

			// last-val sane starting position, this is used by lerp to avoid clicks and pops
			if (aud_buf.in_format == AUDIO_U8)
			{
				aud_buf.last_val[0] = 128;
				aud_buf.last_val[1] = 128;
			} else {
				aud_buf.last_val[0] = 0;
				aud_buf.last_val[1] = 0;
			}

			// get the audio format in use - this will update output values to match what's actually opened
			//  print an error if something went wrong
			if(!Mix_QuerySpec(&aud_buf.out_rate, &aud_buf.out_format, &aud_buf.out_channels))
			{
				printf("Mix_QuerySpec: %s\n",Mix_GetError());
			}

			/* begin filling the audio queue */
			aud_buf.index = 0;

			aud_buf.tail = malloc(sizeof(struct smk_audio_stream_component));
			aud_buf.tail->next = NULL;
			aud_buf.tail->len = smk_get_audio_size(s,0);
			aud_buf.tail->data = malloc(aud_buf.tail->len);
			memcpy(aud_buf.tail->data,smk_get_audio(s,0),aud_buf.tail->len);
			aud_buf.head = aud_buf.tail;

			// Start the audio stream-player
			Mix_HookMusic(movie_music_hook, &aud_buf);
		}

		// Start the video timer
		frame_timer = SDL_AddTimer((Uint32)((usf / 1000) + .5), next_frame, NULL);

		SDL_Event event;
		while (!done)
		{
			/* Keypress skips to video 2 */
			while (SDL_PollEvent (&event))
			{
				switch (event.type)
				{
					case SDL_USEREVENT:
						/* SDL_USEREVENT is triggered when the frame timer fires
							Time to update the scene */
						if (smk_next(s) == SMK_DONE)
						{
							// movie over, out of frames
							done = next_gamestate;
						} else {
							if (audio_exists)
							{
								struct smk_audio_stream_component *tmp = malloc(sizeof(struct smk_audio_stream_component));
								tmp->next = NULL;
								tmp->len = smk_get_audio_size(s,0);
								tmp->data = malloc(tmp->len);
								memcpy(tmp->data,smk_get_audio(s,0),tmp->len);

								if (aud_buf.head == NULL)
								{
									aud_buf.head = aud_buf.tail = tmp;
//									aud_buf.index = 0;
								} else {
									aud_buf.tail->next = tmp;
									aud_buf.tail = tmp;
									//						tmp->next = aud_buf.tail;
									//						aud_buf.tail = tmp;
								}
							}

							frame = tex_from_smkframe(smk_get_palette(s),smk_get_video(s),w,h);
							if (frame == NULL)
							{
								fprintf(stderr,"WARNING: tex_from_smkframe returned NULL.\n");
								done = next_gamestate;
							} else {
								rebuild_texture (frame_tex, frame);
								SDL_FreeSurface(frame);

//								glClear(GL_COLOR_BUFFER_BIT);

								glCallList(quad_list);

								SDL_GL_SwapBuffers();
							}
						}
						break;
					case SDL_KEYUP:
						done = next_gamestate;
						break;
					case SDL_QUIT:
						done = GAMESTATE_EXIT;
						break;
					default:
						break;
				}
			}

			/* Don't eat all the CPU */
			SDL_Delay (1);
		}

		/* shutdown frame timer */
		SDL_RemoveTimer(frame_timer);

		if (audio_exists)
		{
			// unhook music
			Mix_HookMusic(NULL, NULL);
			// beginning from head, trash all the audio data
			struct smk_audio_stream_component *tmp = aud_buf.head;
			while (tmp != NULL)
			{
				tmp = tmp->next;
				if (aud_buf.head->data != NULL) free(aud_buf.head->data);
				free(aud_buf.head);
				aud_buf.head = tmp;
			}
		}

		/* delete the quad list */
		glDeleteLists(quad_list, 1);

		/* delete the frame texture */
		glDeleteTextures( 1, &frame_tex );
	}

	/* close SMK, all done! */
	smk_close(s);

	return done;
}
